/*
 * Created on 20-Dec-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.instancemanager.impl;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.SystemProperties;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.instancemanager.AZInstanceManagerAdapter;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.plugins.dht.DHTPlugin;
import com.aelitis.azureus.plugins.dht.DHTPluginContact;
import com.aelitis.azureus.plugins.dht.DHTPluginListener;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;

public class AZMyInstanceImpl extends AZInstanceImpl {
    public static final long FORCE_READ_EXT_MIN = 8 * 60 * 60 * 1000;
    public static final long UPNP_READ_MIN = 5 * 60 * 1000;

    private AZInstanceManagerAdapter adapter;
    private AZInstanceManagerImpl manager;

    private String id;
    private InetAddress internal_address;
    private int tcp_port;
    private int udp_port;
    private int udp_non_data_port;

    private long last_upnp_read;

    private InetAddress dht_address;
    private long dht_address_time;

    private long last_force_read_ext;
    private InetAddress last_external_address;

    protected AZMyInstanceImpl(AZInstanceManagerAdapter _adapter, AZInstanceManagerImpl _manager)

    {
        adapter = _adapter;
        manager = _manager;

        id = adapter.getID();

        if (id.length() == 0) {

            id = "" + SystemTime.getCurrentTime();
        }

        id = ByteFormatter.encodeString(new SHA1Simple().calculateHash(id.getBytes()));

        COConfigurationManager.addListener(new COConfigurationListener() {
            public void configurationSaved() {
                readConfig(false);
            }
        });

        readConfig(true);

        adapter.addListener(new AZInstanceManagerAdapter.StateListener() {
            public void started() {
                DHTPlugin dht = adapter.getDHTPlugin();

                if (dht != null) {

                    dht.addListener(new DHTPluginListener() {
                        public void localAddressChanged(DHTPluginContact local_contact) {
                            InetAddress latest_dht_address = local_contact.getAddress().getAddress();

                            if (sameFamily(internal_address, latest_dht_address)) {

                                dht_address = latest_dht_address;
                                dht_address_time = SystemTime.getCurrentTime();

                                manager.informChanged(AZMyInstanceImpl.this);
                            }
                        }
                    });
                }
            }

            public void stopped() {
            }
        });
    }

    private void readConfig(boolean first_time) {
        InetAddress new_internal_address = NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress();

        if (new_internal_address == null) {

            try {
                new_internal_address = InetAddress.getByName("0.0.0.0");

            } catch (Throwable e) {
            }
        }

        int[] ports = adapter.getPorts();

        int new_tcp_port = ports[0];
        int new_udp_port = ports[1];
        int new_udp_non_data_port = ports[2];

        boolean same = true;

        if (!first_time) {

            same =
                    internal_address.equals(new_internal_address) && tcp_port == new_tcp_port && udp_port == new_udp_port
                            && udp_non_data_port == new_udp_non_data_port;
        }

        internal_address = new_internal_address;
        tcp_port = new_tcp_port;
        udp_port = new_udp_port;
        udp_non_data_port = new_udp_non_data_port;

        if (!same) {

            manager.informChanged(this);
        }
    }

    private InetAddress readExternalAddress() {
        InetAddress external_address = null;

        // no point in kicking off any queries if we're closing

        if (manager.isClosing()) {

            external_address = last_external_address;

            if (external_address == null) {

                try {
                    external_address = InetAddress.getByName("127.0.0.1");

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }

            return (external_address);
        }

        DHTPlugin dht = adapter.getDHTPlugin();

        // if DHT has informed us of an address then we use this - most reliable up to date one
        // unless the version server cache time is more recent

        if (dht_address != null && dht_address_time <= SystemTime.getCurrentTime()) {

            AZInstanceManagerAdapter.VCPublicAddress a = adapter.getVCPublicAddress();

            if (a != null) {

                long cache_time = a.getCacheTime();

                if (cache_time <= dht_address_time) {

                    external_address = dht_address;
                }
            }
        }

        if (external_address == null && (dht == null || dht.getStatus() != DHTPlugin.STATUS_RUNNING)) {

            // use cached version if available and the DHT isn't

            AZInstanceManagerAdapter.VCPublicAddress a = adapter.getVCPublicAddress();

            if (a != null) {

                try {

                    external_address = InetAddress.getByName(a.getAddress());

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }

        if (external_address == null && dht != null) {

            // no cache, use DHT (this will hang during initialisation, hence the use of cached
            // version above

            try {
                InetAddress latest_dht_address = dht.getLocalAddress().getAddress().getAddress();

                // ignore any v6 addresses from DHT

                if (sameFamily(internal_address, latest_dht_address)) {

                    external_address = latest_dht_address;
                }

            } catch (Throwable e) {
            }
        }

        long now = SystemTime.getCurrentTime();

        if (last_force_read_ext > now) {

            last_force_read_ext = now;
        }

        boolean ok_to_try_ext = now - last_force_read_ext > FORCE_READ_EXT_MIN;

        // try upnp - limit frequency unless external read is possible in which
        // case we try upnp first
        // currently we only use UPnP to validate our current external address, not
        // to deduce new ones (as for example there may be multiple upnp devices and
        // we don't know which one to believe

        if (external_address == null && last_external_address != null) {

            if (last_upnp_read > now) {

                last_upnp_read = now;
            }

            if (now - last_upnp_read > UPNP_READ_MIN || ok_to_try_ext) {

                last_upnp_read = now;

                try {
                    UPnPPlugin upnp = adapter.getUPnPPlugin();

                    if (upnp != null) {

                        String[] addresses = upnp.getExternalIPAddresses();

                        for (int i = 0; i < addresses.length; i++) {

                            if (addresses[i].equals(last_external_address.getHostAddress())) {

                                external_address = last_external_address;

                                break;
                            }
                        }
                    }
                } catch (Throwable e) {
                }
            }
        }

        if (external_address == null) {

            // force read it

            if (ok_to_try_ext) {

                last_force_read_ext = now;

                external_address = adapter.getPublicAddress();
            }
        }

        // no good address available

        if (external_address == null) {

            if (last_external_address != null) {

                external_address = last_external_address;

            } else {
                try {
                    external_address = InetAddress.getByName("127.0.0.1");

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        } else {

            last_external_address = external_address;
        }

        return (external_address);
    }

    private boolean sameFamily(InetAddress a1, InetAddress a2) {
        return (a1 instanceof Inet4Address == a2 instanceof Inet4Address);
    }

    public String getID() {
        return (id);
    }

    public String getApplicationID() {
        return (SystemProperties.getApplicationIdentifier() + "_" + SystemProperties.getApplicationVersion());
    }

    public InetAddress getInternalAddress() {
        return (internal_address);
    }

    public List getInternalAddresses() {
        List l = new ArrayList();

        if (internal_address != null) {

            l.add(internal_address);
        }

        return (l);
    }

    public InetAddress getExternalAddress() {
        return (readExternalAddress());
    }

    public int getTCPListenPort() {
        return (tcp_port);
    }

    public int getUDPListenPort() {
        return (udp_port);
    }

    public int getUDPNonDataListenPort() {
        return (udp_non_data_port);
    }

    public Map<String, Object> getProperties() {
        return ((Map<String, Object>) COConfigurationManager.getMapParameter("instance.manager.props", null));
    }
}
